package client

import (
	"fmt"
	"learning/gooop/etcd/raft/common"
	"learning/gooop/etcd/raft/debug"
	"learning/gooop/etcd/raft/model"
	"learning/gooop/etcd/raft/rpc"
	"learning/gooop/etcd/raft/timeout"
	"sync"
	"time"
)

type tConnectedState struct {
	model.TEventDrivenModel
	context iStateContext

	mInitOnce  sync.Once
	mStartOnce sync.Once

	// update: ceInit, ceDisposing
	mDisposedFlag bool
}

// trigger: init()
// args: empty
const ceInit = "connected.init"

// trigger: Start()
// args: empty
const ceStart = "connected.Start"

// trigger:
// args: empty
const ceDisposing = "connected.Disposing"

// trigger: whenStartThenBeginPing()
// args: empty
const cePingFailed = "connected.PingFailed"

func newConnectedState(ctx iStateContext) iClientState {
	it := new(tConnectedState)
	it.init(ctx)
	return it
}

func (me *tConnectedState) init(ctx iStateContext) {
	me.mInitOnce.Do(func() {
		me.context = ctx
		me.initEventHandlers()
		me.Raise(ceInit)
	})
}

func (me *tConnectedState) initEventHandlers() {
	// write only logic
	me.hookEventsForDisposedFlag()

	// read only logic
	me.Hook(ceStart,
		me.whenStartThenBeginPing)

	me.Hook(cePingFailed,
		me.whenPingFailedThenSwitchToBrokenState)

	me.Hook(ceDisposing,
		me.whenDisposingThenCloseConn)
}

func (me *tConnectedState) Start() {
	me.mStartOnce.Do(func() {
		me.Raise(ceStart)
	})
}

func (me *tConnectedState) hookEventsForDisposedFlag() {
	me.Hook(ceInit, func(e string, args ...interface{}) {
		me.mDisposedFlag = false
	})

	me.Hook(ceDisposing, func(e string, args ...interface{}) {
		me.mDisposedFlag = true
	})
}

func (me *tConnectedState) whenStartThenBeginPing(_ string, _ ...interface{}) {
	go func() {
		cmd := &rpc.PingCmd{
			SenderID:  me.context.Config().GetID(),
			Timestamp: time.Now().UnixNano(),
		}
		ret := &rpc.PingRet{}
		for range time.Tick(timeout.ClientPingInterval) {
			if me.mDisposedFlag {
				return
			}
			if debug.KilledNodeID == me.context.Config().GetID() {
				return
			}

			cmd.Timestamp = time.Now().UnixNano()
			err := me.Ping(cmd, ret)
			if err != nil {
				me.Raise(cePingFailed)
			}
		}
	}()
}

func (me *tConnectedState) whenPingFailedThenSwitchToBrokenState(_ string, _ ...interface{}) {
	me.Raise(ceDisposing)
	me.context.HandleStateChanged(newBrokenState(me.context))
}

func (me *tConnectedState) whenDisposingThenCloseConn(_ string, _ ...interface{}) {
	it := me.context.GetConn()
	if it != nil {
		it.Close()
	}

	me.context.SetConn(nil)
}

func (me *tConnectedState) Heartbeat(cmd *rpc.HeartbeatCmd, ret *rpc.HeartbeatRet) error {
	return me.context.GetConn().Call(fmt.Sprintf("%s.Heartbeat", common.RaftRPCServer_ClassName), cmd, ret)
}

func (me *tConnectedState) AppendLog(cmd *rpc.AppendLogCmd, ret *rpc.AppendLogRet) error {
	return me.context.GetConn().Call(fmt.Sprintf("%s.AppendLog", common.RaftRPCServer_ClassName), cmd, ret)
}

func (me *tConnectedState) CommitLog(cmd *rpc.CommitLogCmd, ret *rpc.CommitLogRet) error {
	return me.context.GetConn().Call(fmt.Sprintf("%s.CommitLog", common.RaftRPCServer_ClassName), cmd, ret)
}

func (me *tConnectedState) RequestVote(cmd *rpc.RequestVoteCmd, ret *rpc.RequestVoteRet) error {
	return me.context.GetConn().Call(fmt.Sprintf("%s.RequestVote", common.RaftRPCServer_ClassName), cmd, ret)
}

func (me *tConnectedState) Ping(cmd *rpc.PingCmd, ret *rpc.PingRet) error {
	return me.context.GetConn().Call(fmt.Sprintf("%s.Ping", common.RaftRPCServer_ClassName), cmd, ret)
}

func (me *tConnectedState) ExecuteKVCmd(cmd *rpc.KVCmd, ret *rpc.KVRet) error {
	return me.context.GetConn().Call(fmt.Sprintf("%s.ExecuteKVCmd", common.KVStoreRPCServer_ClassName), cmd, ret)
}
